﻿// <copyright file="LogicalExpressionParser.cs" company="http://www.dymetis.com">
//
// Copyright (c) 2011 Eddie Z. Lin.
//
// This source is subject to the Microsoft Public License.
// See http://www.opensource.org/licenses/ms-pl.
// All other rights reserved.
// 
// </copyright>

namespace Dymetis.Expressions
{
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;

    internal sealed class LogicalExpressionParser
    {
        private readonly LogicalExpressionReader reader;

        private readonly LogicalExpressionSyntax syntax;

        private readonly Dictionary<string, ParameterExpression> parameters = new Dictionary<string, ParameterExpression>();

        private static readonly HashSet<LogicalExpressionTokenType> booleanOperators = new HashSet<LogicalExpressionTokenType>()
        { 
            LogicalExpressionTokenType.OperatorLessThan,
            LogicalExpressionTokenType.OperatorGreaterThan,
            LogicalExpressionTokenType.OperatorLessThanEqual,
            LogicalExpressionTokenType.OperatorGreaterThanEqual,
            LogicalExpressionTokenType.OperatorEqual,
            LogicalExpressionTokenType.OperatorNotEqual,
            LogicalExpressionTokenType.OperatorUnder,
            LogicalExpressionTokenType.OperatorMatch,
            LogicalExpressionTokenType.OperatorLike
        };

        private LogicalExpressionToken currentToken;

        public LogicalExpressionParser(string input, LogicalExpressionSyntax syntax)
        {
            this.syntax = syntax;

            this.reader = new LogicalExpressionReader(input, this.syntax);
        }

        public LambdaExpression Parse()
        {
            this.ReadToken();

            var expression = this.ParseOrExpression();

            if (this.currentToken != null)
            {
                throw new Exception();
            }

            return Expression.Lambda(expression, this.parameters.Values);
        }

        private static Expression ChangeType(Expression expression, Type targetType)
        {
            return Expression.Call(LogicalExpressionHelper.ChangeTypeMethod.MakeGenericMethod(targetType, expression.Type), expression);
        }

        private Expression ParseOrExpression()
        {
            var orExpression = this.ParseAndExpression();

            while (this.currentToken != null && this.currentToken.TokenType == LogicalExpressionTokenType.OperatorOr)
            {
                this.ReadToken();

                orExpression = Expression.Or(
                   orExpression,
                   this.ParseAndExpression());
            }

            return orExpression;
        }

        private Expression ParseAndExpression()
        {
            var andExpression = this.ParseNotExpression();

            while (this.currentToken != null && this.currentToken.TokenType == LogicalExpressionTokenType.OperatorAnd)
            {
                this.ReadToken();

                andExpression = Expression.And(andExpression, this.ParseNotExpression());
            }

            return andExpression;
        }

        private Expression ParseNotExpression()
        {
            if (this.currentToken != null && this.currentToken.TokenType == LogicalExpressionTokenType.OperatorNot)
            {
                this.ReadToken();
                return Expression.Not(this.ParseNestedExpression());
            }

            return this.ParseNestedExpression();
        }

        private Expression ParseNestedExpression()
        {
            if (this.currentToken == null)
            {
                throw new Exception("Syntax error");
            }

            if (this.currentToken.TokenType == LogicalExpressionTokenType.ParenthesisLeft)
            {
                this.ReadToken();
                var innerExpression = this.ParseOrExpression();
                this.CheckAndReadToken(LogicalExpressionTokenType.ParenthesisRight);

                return innerExpression;
            }

            return this.ParseCondition();
        }

        private Expression ParseOperand()
        {
            Expression expression = null;
            var token = this.currentToken;
            this.ReadToken();

            switch (token.TokenType)
            {
                case LogicalExpressionTokenType.VariableIndicator:

                    ParameterExpression paramaterExpression;
                    if (!this.parameters.TryGetValue(this.currentToken.Spelling, out paramaterExpression))
                    {
                        paramaterExpression = Expression.Parameter(typeof(object), this.currentToken.Spelling);
                        this.parameters.Add(this.currentToken.Spelling, paramaterExpression);
                    }

                    this.ReadToken();

                    return paramaterExpression;

                case LogicalExpressionTokenType.LiteralInteger:
                    return Expression.Constant(Int64.Parse(token.Spelling), typeof(long));

                case LogicalExpressionTokenType.LiteralDecimal:
                    return Expression.Constant(Decimal.Parse(token.Spelling), typeof(decimal));

                case LogicalExpressionTokenType.OperatorDate:
                    this.CheckAndReadToken(LogicalExpressionTokenType.ParenthesisLeft);
                    expression = Expression.Constant(DateTime.Parse(this.currentToken.Spelling), typeof(DateTime));
                    this.ReadToken();
                    this.CheckAndReadToken(LogicalExpressionTokenType.ParenthesisRight);
                    return expression;

                case LogicalExpressionTokenType.ConstantNull:
                    return Expression.Constant(null, typeof(object));

                case LogicalExpressionTokenType.ConstantToday:
                    expression = Expression.Call(LogicalExpressionHelper.DateGetDateMethod);
                    if (this.currentToken != null)
                    {
                        var operatorTokenType = this.currentToken.TokenType;

                        if (operatorTokenType == LogicalExpressionTokenType.OperatorAdd || operatorTokenType == LogicalExpressionTokenType.OperatorSubtract)
                        {
                            // Read day offset.
                            this.ReadToken();
                            var offset = Int64.Parse(this.currentToken.Spelling);

                            this.ReadToken();

                            if (operatorTokenType == LogicalExpressionTokenType.OperatorAdd)
                            {
                                expression = Expression.Call(LogicalExpressionHelper.DateAddDaysMethod, expression, Expression.Constant(offset, typeof(long)));
                            }
                            else if (operatorTokenType == LogicalExpressionTokenType.OperatorSubtract)
                            {
                                expression = Expression.Call(LogicalExpressionHelper.DateSubstractDaysMethod, expression, Expression.Constant(offset, typeof(long)));
                            }
                        }
                    }

                    return expression;

                case LogicalExpressionTokenType.LiteralString:
                    return Expression.Constant(token.Spelling, typeof(string));
            }

            throw new Exception("Syntax error");
        }

        private Expression ParseCondition()
        {
            var leftOperand = this.ParseOperand();

            if (this.syntax.LeftOperandAsVariable && leftOperand.NodeType == ExpressionType.Constant && leftOperand.Type == typeof(string))
            {
                var parameterName = (ConstantExpression)leftOperand;
                ParameterExpression paramaterExpression;
                if (!this.parameters.TryGetValue(parameterName.Value.ToString(), out paramaterExpression))
                {
                    paramaterExpression = Expression.Parameter(typeof(object), parameterName.Value.ToString());
                    this.parameters.Add(parameterName.Value.ToString(), paramaterExpression);
                }

                leftOperand = paramaterExpression;
            }

            var operatorToken = this.currentToken;
            if (operatorToken == null || !booleanOperators.Contains(operatorToken.TokenType))
            {
                throw new Exception("Syntax error");
            }

            this.ReadToken();

            var rightOperand = this.ParseOperand();

            if (leftOperand.Type != typeof(object))
            {
                leftOperand = ChangeType(leftOperand, typeof(object));
            }

            if (rightOperand.Type != typeof(object))
            {
                rightOperand = ChangeType(rightOperand, typeof(object));
            }

            switch (operatorToken.TokenType)
            {
                case LogicalExpressionTokenType.OperatorLessThan:
                    return Expression.Call(LogicalExpressionHelper.ObjectLessThanMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorGreaterThan:
                    return Expression.Call(LogicalExpressionHelper.ObjectGreaterThanMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorLessThanEqual:
                    return Expression.Call(LogicalExpressionHelper.ObjectLessThanEqualMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorGreaterThanEqual:
                    return Expression.Call(LogicalExpressionHelper.ObjectGreaterThanEqualMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorEqual:
                    return Expression.Call(LogicalExpressionHelper.ObjectEqualMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorNotEqual:
                    return Expression.Call(LogicalExpressionHelper.ObjectNotEqualMethod, leftOperand, rightOperand);

                // String operators.
                case LogicalExpressionTokenType.OperatorMatch:
                    return Expression.Call(LogicalExpressionHelper.StringIsMatchMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorLike:
                    return Expression.Call(LogicalExpressionHelper.StringIsLikeMethod, leftOperand, rightOperand);

                case LogicalExpressionTokenType.OperatorUnder:
                    return Expression.Call(LogicalExpressionHelper.StringIsUnderMethod, leftOperand, rightOperand);
            }

            throw new Exception("Syntax error");
        }

        private void ReadToken()
        {
            this.currentToken = this.reader.ReadToken();
        }

        private void CheckAndReadToken(LogicalExpressionTokenType tokenType)
        {
            if (this.currentToken.TokenType != tokenType)
            {
                throw new Exception("Syntax error");
            }

            this.ReadToken();
        }
    }
}
